package cn.csc.framework.pay.core.client.impl.lkl;

import cn.csc.framework.common.util.json.JsonUtils;
import cn.csc.framework.pay.core.client.AbstractPayCodeMapping;
import cn.csc.framework.pay.core.client.PayCommonResult;
import cn.csc.framework.pay.core.client.dto.*;
import cn.csc.framework.pay.core.client.impl.AbstractPayClient;
import cn.csc.framework.pay.core.client.impl.lkl.client.LKLClient;
import cn.csc.framework.pay.core.enums.PayNotifyRefundStatusEnum;
import cn.csc.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.csc.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.JsonObject;
import com.lkl.laop.sdk.LKLSDK;
import com.lkl.laop.sdk.exception.SDKException;
import com.lkl.laop.sdk.request.V3LabsTradeQueryRequest;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.math.BigInteger;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static cn.hutool.core.date.DatePattern.*;

/**
 * 拉卡拉支付抽象类， 实现拉卡拉支付统一的接口。如退款
 * @author liuchuang
 * @version 1.0
 * @date 2023/11/1 17:59
 * @modify
 */
@Slf4j
public abstract class AbstractLklpayClient<L> extends AbstractPayClient<LKLPayClientConfig> {

    /**
     * 网关地址 - 线上
     */
    public static final String SERVER_URL_PROD = "https://s2.lakala.com";
    /**
     * 网关地址 - 测试
     */
    public static final String SERVER_URL_TEST = "https://test.wsmsd.cn/sit";

    /**
     * 参考 {@link LKLSDK}
     */
    @Getter
    protected LKLClient client;

    public AbstractLklpayClient(BigInteger channelId ,String channelCode, LKLPayClientConfig config, AbstractPayCodeMapping codeMapping) {
        super(channelId, channelCode, config, codeMapping);
    }

    @Override
    @SneakyThrows
    protected void doInit() {
        config.setServerUrl(Optional.ofNullable(config.getServerUrl()).orElse(SERVER_URL_PROD));
        config.setNotifyCertificate(config.getCertificate());
        log.info("[AbstractLklpayClient.doInit]config:{}",getId());
        this.client = new LKLClient(config);
        log.info("[AbstractLklpayClient.doInit]client:{}",getId());
    }

    /**
     * 从拉卡拉通知返回参数中解析 PayOrderNotifyRespDTO, 通知具体参数参考
     *  http://open.lakala.com/#/home/document/detail?title=%E6%8E%A5%E5%85%A5%E6%B5%81%E7%A8%8B&id=117
     * @param data 通知结果
     * @return 解析结果 PayOrderNotifyRespDTO
     * @throws Exception  解析失败，抛出异常
     */
    @Override
    public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
        JSONObject bodyJson = JSONObject.parseObject(data.getBody());

        return  PayOrderNotifyRespDTO.builder().orderExtensionNo(bodyJson.getString("out_trade_no"))
                .channelOrderNo(bodyJson.getString("trade_no")).channelUserId(bodyJson.getString("settle_merchant_no"))
                .tradeStatus(bodyJson.getString("trade_status"))
                .successTime(LocalDateTimeUtil.parse(bodyJson.getString("trade_time"), PURE_DATETIME_PATTERN))
                .rawData(data.getBody()).build();
    }

    @Override
    public PayRefundNotifyRespDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
        JSONObject bodyJson = JSONObject.parseObject(notifyData.getBody());
        PayRefundNotifyRespDTO notifyDTO = PayRefundNotifyRespDTO.builder().channelOrderNo(bodyJson.getString("trade_no"))
                .tradeNo(bodyJson.getString("out_trade_no"))
                .reqNo(bodyJson.getString("log_no"))
                .status(PayNotifyRefundStatusEnum.SUCCESS)
                .refundSuccessTime(LocalDateTimeUtil.parse(bodyJson.getString("trade_time"), PURE_DATETIME_PATTERN))
                .rawData(notifyData.getBody())
                .build();
        return notifyDTO;
    }

    // http://open.lakala.com/#/home/document/detail?title=%E6%8E%A5%E5%85%A5%E6%B5%81%E7%A8%8B&id=117
    @Override
    public boolean isRefundNotify(PayNotifyDataDTO notifyData) {
        JSONObject bodyJson = JSONObject.parseObject(notifyData.getBody());
        if (bodyJson.containsKey("trade_status") &&
                ("PART_REFUND".equals(bodyJson.getString("trade_status")) || "REFUND".equals(bodyJson.getString("trade_status"))) ) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 验证是否渠道通知
     * 拉卡拉对request请求进行验签 {@link LKLSDK#notificationHandle(HttpServletRequest)}
     * @param notifyData 通知数据
     * @return
     */
    @Override
    public boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
        return super.verifyNotifyData(notifyData);
    }

    /**
     * 拉卡拉统一的退款接口
     * http://open.lakala.com/#/home/document/detail?title=%E6%8E%A5%E5%85%A5%E8%A7%84%E8%8C%83&id=113
     * @param reqDTO 退款请求 request DTO
     * @return 退款请求 Response
     */
    @Override
    protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {

        return null;
    }



    /**
     * 支付宝统一回调参数  str 转 map
     *
     * @param s 支付宝支付通知回调参数
     * @return map 支付宝集合
     */
    public static Map<String, String> strToMap(String s) {
        // TODO @zxy：这个可以使用 hutool 的 HttpUtil decodeParams 方法么？
        Map<String, String> stringStringMap = new HashMap<>();
        // 调整时间格式
        String s3 = s.replaceAll("%3A", ":");
        // 获取 map
        String s4 = s3.replace("+", " ");
        String[] split = s4.split("&");
        for (String s1 : split) {
            String[] split1 = s1.split("=");
            stringStringMap.put(split1[0], split1[1]);
        }
        return stringStringMap;
    }

    /**
     * 退款接口
     * @param reqDTO 退款请求 request DTO
     * @return 退款请求 Response
     */
    @Override
    protected PayRefundUnifiedRespDTO doUnifiedBeanRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {

        return null;
    }

    /**
     * 获得支付订单信息
     * https://o.lakala.com/#/home/document/detail?title=%E6%8E%A5%E5%85%A5%E8%A7%84%E8%8C%83&id=116
     * @param outTradeNo 外部订单号
     * @return 支付订单信息
     */
    @Override
    protected PayOrderUnifiedRespDTO doGetOrder(String outTradeNo) throws Throwable{
        //2. 装配数据
        V3LabsTradeQueryRequest request = new V3LabsTradeQueryRequest();
        request.setMerchantNo(config.getMerchantNo());
        request.setTermNo(config.getTermNo());
        request.setOutTradeNo(outTradeNo);
        request.setTradeNo(null);
        log.info("[doGetOrder][request({})]", JsonUtils.toJsonString(request));
        //3. 发送请求
        JsonObject response;
        try {
            response = client.httpPostJson(request);
        } catch (SDKException e){
            log.error("[doGetOrder][request({}) 发起支付失败]", JsonUtils.toJsonString(request), e);
            throw new RuntimeException(e.getCode(),e);
        }

        //4. 处理结果
        JsonObject respData = response.getAsJsonObject("resp_data");
        // 查询交易中，如果返回响应CODE为BBS00000，仅表示查到了这笔交易。交易本身的成功与否状态，要查看响应报文中的trade_state这个值。
        if ("BBS00000".equals(response.get("code").getAsString()) && "SUCCESS".equals(response.get("trade_state").getAsString())){
            // 实际支付时间。yyyyMMddHHmmss，转换为yyyy-MM-dd HH:mm:ss
            String tradeTime = LocalDateTimeUtil.formatNormal(LocalDateTimeUtil.parse(response.get("trade_time").getAsString(), PURE_DATETIME_FORMATTER));
            return PayOrderUnifiedRespDTO.successOf(respData.get("trade_no").getAsString(),"",
                    tradeTime, respData.get("trade_no").getAsString(),respData.get("out_trade_no").getAsString(),
                    respData.toString(), null).setTradeStatus(respData.get("trade_state").getAsString());
        }

        // merchantOrderNo为V3LabsTradeQueryRequest.outTradeNo
        return PayOrderUnifiedRespDTO.closedOf(response.get("code").getAsString(),response.get("msg").getAsString(),
                respData.get("out_trade_no").getAsString(), respData.toString())
                .setTradeStatus(respData.get("trade_state").getAsString());
    }


    @Override
    @SneakyThrows
    public Object parseNotify(PayNotifyReqDTO rawNotify) {
        // 1. 回调数据
        String body = rawNotify.getBody();
        Map<String, String> params = rawNotify.getParams();
        JSONObject bodyJson = JSONObject.parseObject(body);
        log.info("parseNotify: bodyJson {}", bodyJson.get("out_trade_no"));

        LocalDateTime tradeTime = LocalDateTimeUtil.parse(bodyJson.getString("trade_time"), PURE_DATETIME_PATTERN);
        // 2.1 退款的情况
        if ("REFUND".equals(bodyJson.getString("trade_status")) || "PART_REFUND".equals(bodyJson.get("trade_status"))) {
            // TODO 退款失败通知如何判断
            return PayRefundUnifiedRespDTO.builder()
                    .status(PayRefundStatusRespEnum.SUCCESS.getStatus())
                    .outRefundNo(bodyJson.getString("out_trade_no"))
                    .channelRefundId(bodyJson.getString("trade_no"))
                    .finTime(LocalDateTimeUtil.format(tradeTime, NORM_DATETIME_PATTERN))
                    .rawData(body)
                    .build();
        }
        // 2.2 支付的情况
        if ("SUCCESS".equals(bodyJson.get("trade_status"))){
            // 支付成功
            return PayOrderUnifiedRespDTO.builder()
                    .status(PayOrderStatusRespEnum.SUCCESS.getStatus())
                    .merchantOrderNo(bodyJson.getString("acc_trade_no"))
                    .tradeNo(bodyJson.getString("out_trade_no"))
                    .successTime(LocalDateTimeUtil.format(tradeTime, NORM_DATETIME_PATTERN))
                    .channelUserId(StringUtils.hasLength(bodyJson.getString("user_id1")) ? bodyJson.getString("user_id1"):bodyJson.getString("user_id2"))
                    .rawData(body)
                    .build();
        } else {
            // 支付关闭 适合调用支付渠道失败时
            return PayOrderUnifiedRespDTO.builder()
                    .status(PayOrderStatusRespEnum.CLOSED.getStatus())
                    .merchantOrderNo(bodyJson.getString("acc_trade_no"))
                    .tradeNo(bodyJson.getString("out_trade_no"))
                    .successTime(LocalDateTimeUtil.format(tradeTime, NORM_DATETIME_PATTERN))
                    .channelUserId(StringUtils.hasLength(bodyJson.getString("user_id1")) ? bodyJson.getString("user_id1"):bodyJson.getString("user_id2"))
                    .rawData(body)
                    .build();
        }
    }

    protected LocalDateTime parseTime(String str) {
        // "yyyyMMddHHmmss"
        return LocalDateTimeUtil.parse(str, PURE_DATETIME_PATTERN);
    }

}
